Aluno: Victor Waszczynski¶

Aulas 2 e 3 - Coleta, Organização, Limpeza e Imputação de Dados¶

Professor: Marcos Cesar Gritti
Email: cesargritti@gmail.com

Antes de começar:

  • Caso seu ambiente Anaconda não possua uma das dependências necessárias para a execução do código contigo neste notebook, abra uma célula e execute o comando: !pip install -r ../requirements.txt

Neste módulo vamos aprender sobre:

  1. Como carregar dados de diferentes formatos em memória no nosso ambiente de desenvolvimento;
  2. Análise Exploratória;
  3. Tratamento de Dados;
  4. Imputação de dados;
  5. Normalização e estandardização de variáveis contínuas;

1 - Carregando dados no ambiente de desenvolvimento¶

No dia a dia de trabalho, um Cientista de Dados se depara com diferentes tipos fontes de dados. Nem sempre, em sua equipe, haverá um Engenheiro de Dados disponível para te ajudar a coletar dados de interesse em um formato fácil de integração com seu ambiente de desenvolvimento Python (ou qualquer outro ambiente de desenvolvimento científico), no nosso caso, o Jupyter Notebook. Portanto, é fundamental que você domine os principais formatos e/ou fontes existentes no mercado, para que não dependa de um terceiro para uma rápida prototipação/experimentação.

As principais fontes de dados, encontradas por um profissional da área, são:

  • Arquivos .csv;
  • Arquivos .json;
  • Arquivos .parquet;
  • Base de dados relacional SQL;
  • Base de dados não relacional NoSQL;

Em empresas que seguem a filosofia Data Driven haverá, usualmente, uma pedaço de Software chamado de Camada de Ingestão de Dados. Esta camada, desenvolvida por Engenheiros de Dados, tem por objetivo centralizar diversas fontes de informação bruta (arquivos csv, json, parquet, imagens, audios, etc ...) em um único repositório (ou Buckets). Este repositório centralizado recebe o nome de Data Lake, e é o ponto de partida para processos de ETL (Extract, Transform and Load), e também a forma mais fácil de um Cientista de Dados se servir de dados.

A consulta à base de dados SQL/NoSQL está fora do escopo deste módulo, contudo, com o domínio da linguagem Python para o processamento dos principais tipos de arquivo citados acima, . Tampouco trabalharemos, neste módulo, com processamento de imagens/audio.

1.1 - Arquivo .csv¶

A existência de arquivos csv em Data Lakes não é predominante, pois, apesar de ser um arquivo fácil de se manipular, não é o mais eficiente (redução de espaço em disco e otimização de tempo de leitura). Entretanto, é o tipo de arquivo mais encontrado quando a informação ainda não está disponível no Data Lake (exportação de planilhas Excel, base de dados do IBGE, entre outras).

O arquivo csv é o melhor amigo do Pandas. Para carregar um arquivo em memória, utilizamos a função read_csv

dataset = pd.read_csv("caminho/do/arquivo.csv")
In [ ]:
import pandas as pd

dados_csv = pd.read_csv("dados_brutos.csv")
dados_csv.head()
Out[ ]:
uf tipo cod_localidade feat_1 feat_2 feat_3 feat_4 loc_x loc_y mercado_mais_proximo farmacia_mais_proxima escola_mais_proxima num_penit_4km num_penit_500m idade_imovel area preco
0 RJ Tipo 1 Localidade 4 0.620648 9.967806 4.990882 25.124844 0.081382 0.727021 3603.941384 2002.686030 1124.043113 0.0 0.0 NaN 123.0 1348017
1 SP Tipo 2 Localidade 4 0.817642 12.629695 5.466835 23.444343 0.367980 0.145812 2185.209139 683.811862 2462.825432 0.0 0.0 13.0 143.0 926601
2 RJ Tipo 2 Localidade 3 0.793080 11.292156 4.201919 28.230731 0.332654 0.432904 1025.698339 957.451552 1049.112117 0.0 0.0 12.0 150.0 1627474
3 SC Tipo 1 Localidade 4 0.792435 11.563047 5.459777 22.414837 0.159663 0.884596 NaN 3723.067390 1296.121182 0.0 0.0 8.0 160.0 1201041
4 RN Tipo 1 Localidade 3 0.711696 11.655785 4.891314 25.451251 0.156154 0.836320 3925.306331 705.807343 4178.062758 0.0 0.0 12.0 134.0 1444848

1.2 - Arquivo .json¶

É o formato mais utilizado por Engenheiros de Software, devido à sua compatibilidade com as tecnologias de desenvolvimento de APIs da atualidade. Consequentemente, a quantidade de arquivos json em Data Lakes é volumosa.

No Pandas, importa-se um arquivo json utilizando o comando read_json

In [ ]:
dados_json = pd.read_json("dados_brutos.json")
dados_json.head()
Out[ ]:
uf tipo cod_localidade feat_1 feat_2 feat_3 feat_4 loc_x loc_y mercado_mais_proximo farmacia_mais_proxima escola_mais_proxima num_penit_4km num_penit_500m idade_imovel area preco
700 ES Tipo 1 None 0.638824 10.158127 4.874347 26.151255 0.632733 0.481356 341.680530 3456.862812 2557.124996 0.0 0.0 37.0 NaN 1329074
701 SP Tipo 2 Localidade 4 0.794100 11.467263 4.889458 25.737262 0.290362 0.649488 3121.658324 2711.257761 2635.042549 0.0 0.0 4.0 151.0 980660
702 MS Tipo 1 Localidade 4 0.745027 11.088365 4.644014 26.165747 0.117298 0.131615 1808.463617 1178.930223 1231.387072 0.0 NaN 15.0 170.0 1044861
703 PR Tipo 1 Localidade 1 0.773947 12.182951 5.778339 21.948647 0.521053 0.021927 4189.517081 6402.599591 1738.502238 0.0 0.0 8.0 84.0 1347838
704 RO Tipo 2 Localidade 1 0.686853 10.321383 4.589251 27.254870 0.985792 0.744716 385.176751 1630.761705 3446.457453 0.0 0.0 20.0 168.0 751177

1.3 - Arquivo .parquet¶

É um formato de armazenamento colunar, disponível em todos os projetos do ecossistema Hadoop. Em suma, um arquivo parquet permite armazenar e consultar o arquivo de forma eficiênte, o que justifica seu emprego na construção de Data Lakes.

https://parquet.apache.org/

A API do Pandas é intuitiva! Para carregar um arquivo parquet, utilizamos o método pd.read_parquet

In [ ]:
dados_parquet = pd.read_parquet("dados_brutos.parquet")
dados_parquet.head()
Out[ ]:
uf tipo cod_localidade feat_1 feat_2 feat_3 feat_4 loc_x loc_y mercado_mais_proximo farmacia_mais_proxima escola_mais_proxima num_penit_4km num_penit_500m idade_imovel area preco
0 DF Tipo 1 Localidade 1 0.865418 12.469136 4.428843 28.599545 0.039081 0.967402 6424.017248 2312.613264 922.096367 0.0 0.0 9.0 60.0 1283960
1 SC Tipo 1 Localidade 2 0.794821 11.643225 5.745631 20.793361 0.163092 0.150923 1390.553671 7062.080907 6311.945099 0.0 0.0 6.0 135.0 691992
2 MG Tipo 2 Localidade 1 0.765382 11.246786 4.435433 27.697589 0.519452 0.083601 NaN 5795.794123 1406.914318 0.0 0.0 17.0 162.0 1042605
3 SP Tipo 2 Localidade 1 0.742807 10.807940 4.510181 28.549110 0.094447 0.229071 2811.748941 5112.857979 1954.494335 1.0 0.0 3.0 105.0 1132298
4 DF Tipo 1 Localidade 4 0.708509 10.182098 5.581803 22.486957 0.398567 0.594843 3362.149025 1685.887551 1600.735664 0.0 0.0 5.0 137.0 1133084

1.4 - Exercício¶

Os dados da aula de hoje foram divididos em três arquivos, os quais carregamos nas células anteriores. Pesquise na documentação do Pandas como unir as linhas dos dataframes dados_csv, dados_json e dados_parquet, e um novo dataframe nominado dados.

https://pandas.pydata.org/docs/reference/index.html#api

Dica: Concatenar é a palavra chave de pesquisa

In [ ]:
# Substitua a igualdade abaixo por uma que empilhe as linhas dos três conjuntos de 
# dados que carregamos anteriormente em apenas um conjunto de dados denominado `dados`
#dados = dados_csv.copy()
dados = pd.concat([dados_csv,dados_json,dados_parquet]).reset_index(drop=True)
In [ ]:
dados
Out[ ]:
uf tipo cod_localidade feat_1 feat_2 feat_3 feat_4 loc_x loc_y mercado_mais_proximo farmacia_mais_proxima escola_mais_proxima num_penit_4km num_penit_500m idade_imovel area preco
0 RJ Tipo 1 Localidade 4 0.620648 9.967806 4.990882 25.124844 0.081382 0.727021 3603.941384 2002.686030 1124.043113 0.0 0.0 NaN 123.0 1348017
1 SP Tipo 2 Localidade 4 0.817642 12.629695 5.466835 23.444343 0.367980 0.145812 2185.209139 683.811862 2462.825432 0.0 0.0 13.0 143.0 926601
2 RJ Tipo 2 Localidade 3 0.793080 11.292156 4.201919 28.230731 0.332654 0.432904 1025.698339 957.451552 1049.112117 0.0 0.0 12.0 150.0 1627474
3 SC Tipo 1 Localidade 4 0.792435 11.563047 5.459777 22.414837 0.159663 0.884596 NaN 3723.067390 1296.121182 0.0 0.0 8.0 160.0 1201041
4 RN Tipo 1 Localidade 3 0.711696 11.655785 4.891314 25.451251 0.156154 0.836320 3925.306331 705.807343 4178.062758 0.0 0.0 12.0 134.0 1444848
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
2022 SC Tipo 2 Localidade 4 0.923722 13.940390 5.222921 24.202279 0.047978 0.106754 1835.793637 1085.880075 1389.476784 0.0 0.0 3.0 160.0 889177
2023 PR Tipo 2 Localidade 4 0.621180 9.638953 4.872834 26.311889 0.146383 0.696719 3241.890776 1942.810024 1713.583735 0.0 0.0 6.0 88.0 863004
2024 ES Tipo 1 Localidade 3 0.702689 10.906394 5.002910 24.681539 0.998298 0.747620 3207.183221 1425.618869 1571.548396 0.0 0.0 40.0 97.0 1322275
2025 SC Tipo 1 Tipo 3 0.840306 12.612464 5.191198 23.653668 0.565233 0.801478 2574.736799 1223.924147 150.881637 0.0 0.0 43.0 128.0 1170550
2026 CE Tipo 2 Localidade 1 0.780135 11.537751 4.873889 26.015937 0.835390 0.302851 4390.423348 2626.528938 1230.964520 0.0 0.0 5.0 119.0 791071

2027 rows × 17 columns

2 - Análise exploratória¶

Agora que carregamos os dados no notebook, precisamos explorá-los para encontrar eventuais inconsistências. No dia a dia de trabalho de um Cientista de Dados, é muito comum encontrar:

  • Campos nulos (NaN);
  • Variáveis nominais não padronizadas (ex: "MAÇA", "maça", "MACA", "Maca ");
  • Variáveis contínuas (intervalar ou de razão) fora de escala;
  • Preenchimento incorreto de campos (ex: Espera-se nome da fruta, mas campo foi preenchido com o nome da cor);

Dentre as etapas do processo de Mineração de Dados, a limpeza do conjunto de dados é a que despende maior tempo, e que tem papel chave quanto ao sucesso do projeto. Por quê? Como veremos adiante, alguns algoritmos de Aprendizado de Máquina são gulosos, ou seja, encontrarão uma resposta até mesmo para os ruídos presentes no seu conjunto de treinamento (conceito de Overfitting).

Vamos começar identificando que variáveis existem no conjunto, e seus respectivos tipos, utilizando os comandos:

  • dtypes: para verificar o tipo de cada coluna;
  • sample(N): para coletar uma pequena amostra que pode nos ajudar a sanar dúvidas sobre os tipos;
In [ ]:
dados.dtypes
Out[ ]:
uf                        object
tipo                      object
cod_localidade            object
feat_1                   float64
feat_2                   float64
feat_3                   float64
feat_4                   float64
loc_x                    float64
loc_y                    float64
mercado_mais_proximo     float64
farmacia_mais_proxima    float64
escola_mais_proxima      float64
num_penit_4km            float64
num_penit_500m           float64
idade_imovel             float64
area                     float64
preco                      int64
dtype: object
In [ ]:
dados.sample(5).T
Out[ ]:
977 787 690 823 1598
uf SP None RJ GO SC
tipo Tipo 1 Tipo 2 Tipo 1 Tipo 2 Tipo 2
cod_localidade Localidade 2 Localidade 4 Localidade 4 Localidade 4 Localidade 3
feat_1 0.896537 0.82117 0.816033 NaN 0.870661
feat_2 13.695373 12.409319 13.070487 10.546326 12.524437
feat_3 4.89505 4.742783 5.733456 NaN 5.115338
feat_4 25.926119 25.964902 21.333684 25.021204 23.90915
loc_x 0.31839 0.877227 0.322636 0.00939 0.205602
loc_y 0.629544 0.288078 0.408328 0.041152 0.045514
mercado_mais_proximo 3107.098813 3422.122721 1798.915576 NaN 4229.536893
farmacia_mais_proxima 2404.877557 4566.669067 3010.546472 1748.382604 4693.206369
escola_mais_proxima 2006.047028 1030.689936 1406.330673 2113.389868 1704.841762
num_penit_4km 1.0 0.0 0.0 0.0 0.0
num_penit_500m 0.0 0.0 0.0 0.0 0.0
idade_imovel 11.0 17.0 14.0 25.0 28.0
area 109.0 127.0 86.0 78.0 158.0
preco 842624 959912 1153233 655796 1205894

Nosso conjunto de dados representa uma base imobiliária, e é formado pelas seguintes colunas:

Nome Descrição Tipo
uf O estado onde o imóvel está localizado object (string)
tipo O tipo do imóvel object (string)
cod_localidade Código qualitativo da localidade do imóvel (Consultoria) object (string)
feat_1 Feature 1 (Consultoria) float64
feat_2 Feature 2 (Consultoria) float64
feat_3 Feature 3 (Consultoria) float64
feat_4 Feature 4 (Consultoria) float64
loc_x Coordenada x do imóvel em um mapa local de referência (Consultoria) float64
loc_y Coordenada y do imóvel em um mapa local de referência (Consultoria) float64
mercado_mais_proximo Distância do mercado mais próximo, em metros float64
farmacia_mais_proxima Distância da farmácia mais próxima, em metros float64
escola_mais_proxima Distância da escola mais próxima, em metros float64
num_penit_4km Número de penitenciárias em um raio de 4km do imóvel int64
num_penit_500m Número de penitenciárias em um raio de 500m do imóvel int64
idade_imovel Idade do imóvel int64
area Área do imóvel, em $m^2$ float64
preco O preço do imóvel, em R$ int64
  • O tipo int64, no Python, representa o conjunto dos números naturais $\mathbb{N}$
  • O tipo float64, no Python, representa o conjunto dos números reais $\mathbb{R}$
  • O tipo object, no Python, pode representar uma string ou uma estrutura de dados composta (list, dict, classes customizadas, entre outras)

As variáveis cod_localidade, feat_1, feat_2, feat_3, feat_4, loc_x e loc_y foram elaboradas por um time de consultoria externa especializada em avaliação imobiliária. Sabemos que os códigos de qualidade contidos na coluna cod_localidade são utilizados para segmentar subregiões em níveis de qualidade, contudo, não sabemos se as categorias podem ser interpretadas como variáveis ordinais. Quanto às variáveis numéricas intervalares (feat_1 à feat_4), sabe-se apenas que foram construídas com base em laudos históricos de avaliação de imóveis próximos, e com base em indicadores macro-econômicos.

Não existe uma receita de bolo exata para tratamento de dados, uma vez que a natureza e as regras de negócio variam muito de problema à problema. Para este caso de estudo, vamos começar verificando o conteúdo das variáveis de tipo object (assumindo como premissa de que são categoricas). Como já sabemos que estes campos estão armazenando valores do tipo string, uma boa pergunta inicial seria: Quais os possíveis valores destes campo?

Para responder a esta pergunta, podemos utilizar a função unique:

In [ ]:
# Valores únicos de UF (incluíndo valores nulos, i.e., NaN)
dados.uf.unique()
Out[ ]:
array(['RJ', 'SP', 'SC', 'RN', 'CE', 'MG', 'ES', 'RO', 'DF', 'MS', 'PR',
       'GO', 'AC', 'BA', 'MT', 'AL', 'RS', 'PB', 'TO', 'AM', 'AP', 'SE',
       'RR', 'PI', 'MA', 'PE', 'PA', nan, None], dtype=object)
In [ ]:
# Valores únicos de tipo
dados.tipo.unique()
Out[ ]:
array(['Tipo 1', 'Tipo 2', 'TIPO 1', 'tipo 2', nan, 'TIPO 2',
       'LOCALIDADE 6', 'Localidade 9', 'Localidade 8', 'tipo 1',
       'Localidade 6', 'Tipo 3', 'Tipo 4', None, 'Localidade 10',
       'Localidade 5', 'TIPO 4', 'tipo 3', 'Localidade 7'], dtype=object)
In [ ]:
# Valores únicos de localidade
dados.cod_localidade.unique()
Out[ ]:
array(['Localidade 4', 'Localidade 3', 'localidade 4', 'Localidade 2',
       'Localidade 9', 'Localidade 1', 'Localidade 7', 'localidade 3',
       'LOCALIDADE 2', 'localidade 1', 'LOCALIDADE 1', nan,
       'Localidade 8', 'Tipo 3', 'localidade 2', 'LOCALIDADE 3',
       'localidade 9', 'LOCALIDADE 4', 'Tipo 2', 'Localidade 5',
       'Localidade 6', None, 'localidade 5', 'Tipo 4', 'Localidade 10',
       'tipo 4', 'localidade 7'], dtype=object)
In [ ]:
dados.describe().T
Out[ ]:
count mean std min 25% 50% 75% max
feat_1 2022.0 0.751808 0.103431 0.222897 0.684783 0.754352 8.201856e-01 1.277357e+00
feat_2 2022.0 11.304705 1.654269 3.067246 10.239090 11.362082 1.238432e+01 1.870217e+01
feat_3 2016.0 5.004588 0.514403 3.214858 4.655714 4.999201 5.347276e+00 6.638993e+00
feat_4 2023.0 24.978204 2.627059 15.850045 23.158389 25.002898 2.680118e+01 3.349520e+01
loc_x 2020.0 0.505169 0.288890 0.000530 0.250194 0.508069 7.473581e-01 9.997262e-01
loc_y 2023.0 0.501737 0.286487 0.000855 0.254863 0.510660 7.463950e-01 9.995796e-01
mercado_mais_proximo 1953.0 3086.351775 1714.499709 37.932408 1794.086282 2816.375170 4.090446e+03 9.733443e+03
farmacia_mais_proxima 1966.0 2675.509079 1538.196595 49.678746 1476.145321 2397.733125 3.622657e+03 8.063493e+03
escola_mais_proxima 1964.0 2429.899456 1318.803468 63.839485 1432.287866 2240.663181 3.273253e+03 7.336996e+03
num_penit_4km 2017.0 0.302925 0.468191 0.000000 0.000000 0.000000 1.000000e+00 2.000000e+00
num_penit_500m 2025.0 0.007901 0.088559 0.000000 0.000000 0.000000 0.000000e+00 1.000000e+00
idade_imovel 2022.0 15.410979 12.595984 0.000000 6.000000 11.000000 2.100000e+01 5.000000e+01
area 1968.0 124.730691 40.153210 51.000000 94.000000 120.000000 1.540000e+02 2.460000e+02
preco 2027.0 989262.435619 290317.635521 257922.000000 797689.000000 964698.000000 1.163877e+06 2.140795e+06

O que é quantil/percentil/quartil?

Quantils são pontos estabelecidos em intervalos regulares em uma lista ordenada que informa o percentual de dados abaixo de um limiar em uma amostra.

Exemplo de cálculo de quantil:

Dada uma lista de valores desordenados $[5, 3, 1, 10, 4]$ o quantil nos diz o número índice da lista no qual $x$% da população (elementos da lista) são menores do que o valor apontado pelo índice. Por exemplo, se ordenarmos a lista de forma crescente, obtemos $[1, 3, 4, 5, 10]$.

Para calcular o quantil 0.5 (equivalente a mediana, e também ao segundo quartil), basta encontrar o índice central da lista. Nesta caso, a lista contém 5 elementos, sendo o índice 3 seu elemento central. Logo, nosso $q_{0.5} = 4$, significando que 50% dos dados de nossa amostra são menores que 4.

Equivalências:

Quantil Quartil Percentil
0.25 1 25%
0.5 2 50%
0.75 3 75%

A combinação de quantils com outras propriedades de uma amostra (como a média, valor mínimo, máximo e variância) nos fornece uma visão precisa da distribuição dos dados sob observação. Com o auxílio da biblioteca seaborn, podemos criar representação pictóricas destas informações, como no caso do Diagrama de Caixas (boxplot) e o Diagrama Violino (violinplot) apresentado abaixo:

In [ ]:
import seaborn as sns
import matplotlib.pyplot as plt

# Ajustando o tamanho padrão das imagens e fontes
sns.set(font_scale=1.0, rc={
    "figure.figsize": (10, 6),
})

ax = plt.subplot(121)
sns.boxplot(data=dados[["area"]], ax=ax)
plt.title("Box Plot"); plt.ylim([0, 300]);

ax = plt.subplot(122)
sns.violinplot(data=dados[["area"]], ax=ax)
plt.title("Violin Plot"); plt.ylim([0, 300]);

Podemos utilizar o argumento hue do seaborn para segmentar visualizações por categorias distintas com o emprego de cores.

In [ ]:
fig, ax = plt.subplots()
sns.violinplot(
    data=dados[dados.tipo.isin(["Tipo 1", "Tipo 2"])][["preco", "tipo"]], 
    y="preco", 
    x="tipo", 
    ax=ax
)
plt.title("Distribuição de preço por tipo de imóvel");

Além da distribuição das variáveis do nosso conjunto de dados (Análise Descritiva Univariada), podemos explorar relações entre pares utilizando a visualização pairplot do seaborn (Análise Descritiva Multivariada), que combina Funções de Densidade de Probabilidade (FDP, em inglês, Probability Density Function) com Gráficos de Disperção (scatterplot), como demonstrado na célula a seguir.

In [ ]:
# A função .drop descarta algumas colunas do nosso DataFrame
sns.pairplot(
    dados.drop(columns=["loc_x", "loc_y", "num_penit_4km", "num_penit_500m"])
)
Out[ ]:
<seaborn.axisgrid.PairGrid at 0x1b7e1126b10>

Utilizando a função corr do Pandas, junto com a visualização de mapa de calor do heatmap do seaborn, é possível criar um correlograma para mensurar, visualmente, as correlações entre as variáveis do conjunto de dados. Um adendo: a função corr admite apenas valores numéricos. Para contornar esse problema, podemos usar a função select_dtypes com o argumento exclude="object" para selecionar todas as colunas em que o tipo é diferente de object, ou seja, apenas as colunas que contém valores numéricos.

In [ ]:
corr = dados.select_dtypes(exclude="object").corr().round(2)
fig, ax = plt.subplots(figsize=(12, 12))
sns.heatmap(corr, 
            annot=True,
            square=True, 
            vmax=.8,
            vmin=-.8,
            linewidths=2, 
            cbar_kws={"shrink": .9})
Out[ ]:
<Axes: >

3 - Exercício¶

O conjunto de arquivos de dados imobiliários contém erros sistemáticos, dados faltantes e outliers.

Atividades:

  • Padronize as variáveis categóricas tipo e cod_localidade, remova as linhas com erros sistemáticos, e remove possíveis outliers (tipos ou localidades com frequência baixa);
  • Para cada registro da base de dados, encontre a regiao por meio da coluna uf utilizando a base de estados do ibge disponibilizada para o exercício;
  • Realize a imputação de dados sobre as variáveis feat_1, feat_2, feat_3 e feat_4 utilizando regressão linear simples. Lembre-se que a partir da análise exploratória dos dados observa-se que feat_1 tem correlação com feat_2, assim como feat_3 tem correlação com feat_4, ou seja, precisamos encontrar duas equações:
    • $feat_1 = k_1 + c_1 feat_2$
    • $feat_3 = k_2 + c_2 feat_4$
  • Remova os outliers dos pares de variáveis (feat_1, feat_2) e (feat_3, feat_4) utilizando o método z-score multidimensional;
  • Identifique se existem outras variávies do conjunto com outliers, e, caso existam, remova os outliers citando a natureza da variável e o méteodo utilizado para identificação dos outliers;
  • Após finalizar as atividades citadas anteriormente, remova todos os dados faltantes ainda presentes no conjunto de dados;
  • Realize uma análise exploratória completa do conjunto de dados tratado;
  • Salve o arquivo final em formato parquet com o nome dados_tratados.parquet;

Informações adicionais:

O prazo total para entrega do exercício é de 8 dias corridos, iniciando contagem a partir da data de disponibilização do exercício no Moodle;

A entrega deve ser feita no Moodle. Contudo, o exercício pode ser entregue em um arquivo comprimido OU via link compartilhado de um fork do projeto no github (ou alguma outra ferramenta de versionamento de código). Fica a critério do aluno escolher a melhor forma de entrega;

O arquivo (ou repositório git) da entrega, deve conter, obrigatóriamente:

  • Uma cópia deste notebook (exercicio.ipynb) com a resolução das atividades;
  • O arquivo dados_tratados.parquet, resultando do tratamento final da base de dados;

Importando algumas bibliotecas

In [ ]:
import seaborn as sns
import numpy as np
In [ ]:
dados = dados[~dados[["tipo","cod_localidade"]].isna().any(axis=1)]

Padronizado a notação de escrita e mantido apenas as variáveis Tipo 1 e 2 pois eram as que mais tinham dados

In [ ]:
dados.loc[:,"tipo"] = dados.tipo.str.capitalize()
dados = dados.query("tipo.isin(['Tipo 1','Tipo 2'])")
dados.tipo.value_counts()
Out[ ]:
tipo
Tipo 2    981
Tipo 1    875
Name: count, dtype: int64

Padronizado a notação de localidade e mantido apenas as Localidades 1,2,3 e 4

In [ ]:
dados.loc[:,"cod_localidade"] = dados.cod_localidade.str.capitalize()
dados = dados[dados.cod_localidade.apply(lambda x: False if (isinstance(x,str) and x.startswith("Tipo")) else True)]
dados = dados.query("cod_localidade.isin(['Localidade 1','Localidade 2','Localidade 3','Localidade 4'])")
dados.cod_localidade.value_counts()
Out[ ]:
cod_localidade
Localidade 4    462
Localidade 3    457
Localidade 2    457
Localidade 1    443
Name: count, dtype: int64

Importando os dados do IBGE

In [ ]:
regiao = pd.read_csv("estados.csv")
regiao.head()
Out[ ]:
cod_uf_ibge estado uf regiao qtd_municipios
0 41 PARANA PR REGIAO SUL 399
1 42 SANTA CATARINA SC REGIAO SUL 295
2 43 RIO GRANDE DO SUL RS REGIAO SUL 497
3 15 PARA PA REGIAO NORTE 144
4 13 AMAZONAS AM REGIAO NORTE 62

Verificando se existe algum estado nos dados que não estão na importação do IBGE. Verifica-se a presença alguns campos Nulos na coluna uf, no mais parece estar tudo certo

In [ ]:
dados[~dados.uf.isin(regiao.uf)].uf.describe()
Out[ ]:
count       0
unique      0
top       NaN
freq      NaN
Name: uf, dtype: object

Unindo os dados

In [ ]:
dados = pd.merge(dados,regiao, left_on="uf",right_on="uf")

Verificando a quantidade de dados Nulos

In [ ]:
print("feat_1: ",dados.feat_1.isna().sum())
print("feat_2: ",dados.feat_2.isna().sum())
print("feat_3: ",dados.feat_3.isna().sum())
print("feat_4: ",dados.feat_4.isna().sum(),"\n")

print("feat_1 e feat_2: ",sum(dados.feat_1.isna() & dados.feat_2.isna()))
print("feat_3 e feat_4: ",sum(dados.feat_3.isna() & dados.feat_4.isna()))

dados = dados.drop(dados[dados.feat_3.isna() & dados.feat_4.isna()].index,axis=0)
feat_1:  5
feat_2:  4
feat_3:  11
feat_4:  4 

feat_1 e feat_2:  0
feat_3 e feat_4:  1

Realizando a regressão linear e o preenchimentos dos dados Nulos para as variáveis feat_1 e feat_2

In [ ]:
dados_regressao_01 = dados[~dados[["feat_1","feat_2"]].isna().any(axis=1)].copy()

y_01 = dados_regressao_01.feat_1.values
x_01 = dados_regressao_01.feat_2.values
constante_01 = np.ones(len(y_01))

matriz_01 = np.vstack([constante_01,x_01]).T
k1,c1 = np.linalg.pinv(matriz_01).dot(y_01)

dados.loc[dados.feat_1.isna(),"feat_1"] = k1 + c1*dados.feat_2
dados.loc[dados.feat_2.isna(),"feat_2"] = (dados.feat_1 - k1)/c1

Realizando a regressão linear e o preenchimentos dos dados Nulos para as variáveis feat_3 e feat_4

In [ ]:
dados_regressao_02 = dados[~dados[["feat_3","feat_4"]].isna().any(axis=1)].copy()

y_02 = dados_regressao_02.feat_3.values
x_02 = dados_regressao_02.feat_4.values
constante_02 = np.ones(len(y_02))

matriz_02 = np.vstack([constante_02,x_02]).T
k2,c2 = np.linalg.pinv(matriz_02).dot(y_02)

dados.loc[dados.feat_3.isna(),"feat_3"] = k2 + c2*dados.feat_4
dados.loc[dados.feat_4.isna(),"feat_4"] = (dados.feat_3 - k2)/c2

Verificando como ficou a questão de dados nulos para todas as colunas

In [ ]:
dados.isna().sum(axis=0)
Out[ ]:
uf                        0
tipo                      0
cod_localidade            0
feat_1                    0
feat_2                    0
feat_3                    0
feat_4                    0
loc_x                     6
loc_y                     2
mercado_mais_proximo     63
farmacia_mais_proxima    58
escola_mais_proxima      58
num_penit_4km             8
num_penit_500m            2
idade_imovel              5
area                     48
preco                     0
cod_uf_ibge               0
estado                    0
regiao                    0
qtd_municipios            0
dtype: int64

Plotando um gráfico de dispersão para as variáveis feat_1 e feat_2

In [ ]:
sns.scatterplot(x = dados.feat_2,y = dados.feat_1)
Out[ ]:
<Axes: xlabel='feat_2', ylabel='feat_1'>

Montando a matriz de covariancia e obtendo as medias para analisar os outliers

In [ ]:
matriz_covariancia_1 = np.cov(dados.feat_1,dados.feat_2)
media_feat_1 = np.mean(dados.feat_1)
media_feat_2 = np.mean(dados.feat_2)

Função copiada do arquivo adicional_outliers para ordenar os autovalores e autovetores

In [ ]:
def eigsorted(cov):
    """
    Encontra os auto-valores e auto-vetores de uma matriz de variância-covariância.
    Os auto-valores e auto-vetores nos ajudam a normalizar amostras de distribuições
    multivariadas.
    
    :cov: Matriz de variância-covariância.
    """
    vals, vecs = np.linalg.eigh(cov)
    order = vals.argsort()[::-1]
    return vals[order], vecs[:,order]

Obtendo os autovalores e autovetores e centralizando-os no ponto (0,0)

In [ ]:
autovalores_1, autovetores_1 = eigsorted(matriz_covariancia_1)
feat_1_outliers = dados.feat_1.copy() - media_feat_1
feat_2_outliers = dados.feat_2.copy() - media_feat_2

Rotacionando os dados conforme sentido que melhor expressa a variância das variaveis

In [ ]:
rotacao_1 = np.vstack([feat_1_outliers,feat_2_outliers]).T.dot(autovetores_1)

Normalizando os dados com relação a variância e utilizando a distancia euclidiana para identificar e remover os outliers

In [ ]:
Z_rotacao_1 = rotacao_1 / np.sqrt(np.diag(np.cov(rotacao_1,rowvar=0)))
outliers_1 = np.sqrt((Z_rotacao_1 ** 2).sum(axis=1)) > 2.5
dados = dados.loc[~outliers_1, :]

Plotando as variaveis feat_1 e feat_2 após remoção dos outliers

In [ ]:
sns.scatterplot(x = dados.feat_2,y = dados.feat_1)
Out[ ]:
<Axes: xlabel='feat_2', ylabel='feat_1'>

Plotando um gráfico de dispersão para as variáveis feat_3 e feat_4

In [ ]:
sns.scatterplot(x = dados.feat_4,y = dados.feat_3)
Out[ ]:
<Axes: xlabel='feat_4', ylabel='feat_3'>

Montando a matriz de covariancia e obtendo as medias para analisar os outliers

In [ ]:
matriz_covariancia_2 = np.cov(dados.feat_3,dados.feat_4)
media_feat_3 = np.mean(dados.feat_3)
media_feat_4 = np.mean(dados.feat_4)

Obtendo os autovalores e autovetores e centralizando-os no ponto (0,0)

In [ ]:
autovalores_2, autovetores_2 = eigsorted(matriz_covariancia_2)
feat_3_outliers = dados.feat_3.copy() - media_feat_3
feat_4_outliers = dados.feat_4.copy() - media_feat_4

Rotacionando os dados conforme sentido que melhor expressa a variância das variaveis

In [ ]:
rotacao_2 = np.vstack([feat_3_outliers,feat_4_outliers]).T.dot(autovetores_2)

Normalizando os dados com relação a variância e utilizando a distancia euclidiana para identificar e remover os outliers

In [ ]:
Z_rotacao_2 = rotacao_2 / np.sqrt(np.diag(np.cov(rotacao_2,rowvar=0)))
outliers_2 = np.sqrt((Z_rotacao_2 ** 2).sum(axis=1)) > 2.5
dados = dados.loc[~outliers_2, :]

Plotando um gráfico de dispersão para as variáveis feat_3 e feat_4

In [ ]:
sns.scatterplot(x = dados.feat_4,y = dados.feat_3)
Out[ ]:
<Axes: xlabel='feat_4', ylabel='feat_3'>

Veridicando se algumas outras variaveis númericas possuem outliers, para isso esta sendo utilizado um gráfico Boxplot

In [ ]:
colunas = ['mercado_mais_proximo', 'farmacia_mais_proxima','escola_mais_proxima']
dados[colunas][~dados[colunas].isna().any(axis=1)].boxplot()
Out[ ]:
<Axes: >

Das três variáveis avalidadas acima, constatou-se que todas tinham outliers, por isso foi realizado uma fórmula para remoção de variáveis com Z score acima de 2.5

In [ ]:
def rem_outliers(distancia, threshold=2.5):

    media = np.mean(distancia)
    desvio = np.std(distancia)
    adiciona_na = []

    for d in distancia:
        if ((d - media) / desvio) > threshold:
            adiciona_na.append(np.nan)
        else:
            adiciona_na.append(d)

    return adiciona_na

Aqui esta sendo aplicada a formula acima para as colunas com as variáveis apontadas acima

In [ ]:
dados[colunas] = dados[colunas].apply(rem_outliers,axis=0)

Após isso foi feito um novo gráfico boxplot, constatando poucos resultados agora como outliers

In [ ]:
dados[colunas][~dados[colunas].isna().any(axis=1)].boxplot()
Out[ ]:
<Axes: >

Removendo os outro dados Nulos que ainda estavam presentes no dataset

In [ ]:
dados = dados[~dados.isna().any(axis=1)]
dados.isna().sum(axis=0)
Out[ ]:
uf                       0
tipo                     0
cod_localidade           0
feat_1                   0
feat_2                   0
feat_3                   0
feat_4                   0
loc_x                    0
loc_y                    0
mercado_mais_proximo     0
farmacia_mais_proxima    0
escola_mais_proxima      0
num_penit_4km            0
num_penit_500m           0
idade_imovel             0
area                     0
preco                    0
cod_uf_ibge              0
estado                   0
regiao                   0
qtd_municipios           0
dtype: int64

Resetando os indices do dataset

In [ ]:
dados = dados.reset_index(drop=True)

Verificando com quantos dados ficou o dataset após todas as transformações

In [ ]:
dados.shape
Out[ ]:
(1392, 21)

Plotando um gráfico de preço por localidade, percebe-se que a localidade 3 parece possuir maior valor de imóveis

In [ ]:
sns.boxplot(y = dados.preco, x= dados.cod_localidade)
Out[ ]:
<Axes: xlabel='cod_localidade', ylabel='preco'>

Já com relação ao gráfico que relaciona preço com tipo, percebe-se que imóveis do Tipo 2 apresentam maior variância quando comparados ao Tipo 1

In [ ]:
sns.boxplot(y = dados.preco, x= dados.tipo)
Out[ ]:
<Axes: xlabel='tipo', ylabel='preco'>

Plotando gráficos de disperção para as variáveis mercado_mais_proximo, farmacia_mais_proxima e escola_mais_proxima, parece que as informações não parecem estar correlacionadas

In [ ]:
fig, axs = plt.subplots(1, 3,figsize=(20,5))
sns.scatterplot(x=dados.mercado_mais_proximo, y=dados.farmacia_mais_proxima, ax=axs[0])
sns.scatterplot(x=dados.mercado_mais_proximo, y=dados.escola_mais_proxima, ax=axs[1])
sns.scatterplot(x=dados.farmacia_mais_proxima, y=dados.escola_mais_proxima, ax=axs[2])
Out[ ]:
<Axes: xlabel='farmacia_mais_proxima', ylabel='escola_mais_proxima'>

Com relação as variáveis preço e área, não apresentam padrão claro entre elas

In [ ]:
sns.scatterplot(x=dados.area, y=dados.preco)
Out[ ]:
<Axes: xlabel='area', ylabel='preco'>

Por fim salvando o dataset dados no formato parquet

In [ ]:
dados.to_parquet("dados_tratados.parquet")